<?php
/**
 * Smart Library - Smart Framework
 * http://smart-framework.highet.eu
 *
 * @category   SmartL
 * @package    SmartL_Application
 * @subpackage Config
 * @copyright  Copyright (c) 2005-2008 Smart Framework team - Stepan Sindelar, Vojtech Vit
 * @license    http://www.opensource.org/licenses/bsd-license.php	New BSD License
 */

/**
 * @see SmartL_Config
 */
require_once 'SmartL/Config.php';
/**
 * @see SmartL_Application_Config_LanguageInfo
 */
require_once 'SmartL/Application/Config/LanguageInfo.php';
/**
 * @see SmartL_Application_Config_ModuleInfo
 */
require_once 'SmartL/Application/Config/ModuleInfo.php';
/**
 * @see SmartL_Application_Config_RequestInfo
 */
require_once 'SmartL/Application/Config/RequestInfo.php';
/**
 * @see SmartL_Application_Config_TemplateInfo
 */
require_once 'SmartL/Application/Config/TemplateInfo.php';
/**
 * @see SmartL_Application_Config_CacheAdapterInfo
 */
require_once 'SmartL/Application/Config/CacheAdapterInfo.php';

/**
 * Parent class for SmartL_Application. It behaves as a configuration storage
 * which supports intellisense-like tooltips (thanks to php-doc comments).
 *
 * All paths in config are relative from top application folder and all paths
 * do not begin with slah and do not end with slash.
 *
 * @todo 7 : (?) Tato třída by zjevně měla podporovat rozhraní Iterator, Countable, ArrayAccess -
 * mozna nemela, kdyz je jen "obalem" tyhle veci obsahuje SmartL_Config a ta se da ziskat pres getConfig
 * @todo 9 : (?) Nebylo by jednodušší, kdyby tato třída dědila od SmartL_Config?
 * Pozn: Třída by tím získala mnoho public metod, které by byly asi nadbytečné. SmartL_App_Config
 * proto slouží jako "pouzdro" na SmartL_Config, které je odfiltruje.
 * @todo : Byloby hezké dodělat podporu dalším konfiguračním možnostem. Třeba nastavení výchozího controlleru a akce u jednotlivých modulů. Nebo přidat nastavení "vždy automaticky načítat překlady", aby se nemusely zbytečně vytvářet konfigurace controllerů.
 * @todo : Možnost nastavení zapnutí Layout na úrovni akcí
 * @todo : Revidovat strukturu konfiguračních tříd. Mohly by lépe reflektovat hierarchickou strukturu.
 * 
 *
 * @see SmartL_Application
 * @uses SmartL_Application_Config_LanguageInfo
 * @uses SmartL_Application_Config_ModuleInfo
 * @uses SmartL_Application_Config_RequestInfo
 * @uses SmartL_Application_Config_TemplateInfo
 *
 * @package SmartL_Application
 * @subpackage Config
 */
class SmartL_Application_Config {
	/**
	 * Class containing all configuration information
	 *
	 * @var SmartL_Config Configuration storage
	 */
	private $_config = null;
	/**
	 * Languages information classes
	 *
	 * @var array [language-CULTURE id] => SmartL_Application_Config_LanguageInfo
	 */
	private $_languagesInfo = null;
	/**
	 * Contains information about default culture of each language
	 *
	 * @var array [language id] => SmartL_Application_Config_LanguageInfo
	 */
	private $_defaultCultures = null;
	/**
	 * Default language information class
	 *
	 * @var SmartL_Application_Config_LanguageInfo Default language info
	 */
	private $_defaultLanguage = null;
	/**
	 * Connections
	 *
	 * @var array [connection name] => array config of connection
	 */
	private $_connections = null;
	/**
	 * Default connection string name = index.
	 *
	 * @var string
	 */
	private $_defaultConnectionStringName = null;
	/**
	 * Modules information classes
	 *
	 * @var array [module name] => SmartL_Application_Config_ModuleInfo
	 */
	private $_modulesInfo = null;
	/**
	 * Default module information class
	 *
	 * @var SmartL_Application_Config_ModuleInfo Default module info
	 */
	private $_defaultModule = null;
	/**
	 * Templates information classes
	 *
	 * @var array [template name] => SmartL_Application_Config_TemplateInfo
	 */
	private $_templates = null;
	/**
	 * Default template information class.
	 *
	 * @var SmartL_Application_Config_TemplateInfo Default template info
	 */
	private $_defaultTemplate = null;
	/**
	 * Array with configuration for cache adapters.
	 *
	 * @var array
	 */
	private $_cacheAdapters = null;
	
	/**
	 * Class constructor, creates configuration information storage.
	 * Constructor is protected because this class is used only by
	 * SmartL_Application, which implements Singleton design pattern.
	 *
	 */
	protected function __construct() {
		$this->_config = new SmartL_Config ( );
	}
	/**
	 * Returns instace of SmartL_Config containing all configuration information.
	 *
	 * @return SmartL_Config Configuration storage
	 */
	public function getConfig() {
		return $this->_config;
	}
	/**
	 * Loads new application configuration file (and removes all previous
	 * configurations).
	 *
	 * @param string $filename Configuration file name (path)
	 */
	public function load($filename) {
		$this->_reset ();
		$this->_config->load ( $filename );
		$this->_normalizePaths ();
	}
	/**
	 * Extends current configuration by new one.
	 *
	 * @param string $filename Configuration file name (path)
	 */
	public function extendBy($filename) {
		$this->_reset ();
		$this->_config->extendBy ( $filename );
		$this->_normalizePaths ();
	}
	/**
	 * Returns whether debugging is on.
	 *
	 * @return bool True if debugging is on
	 */
	public function isDebugMode() {
		return (strtolower ( $this->_config->debug->getValue () ) == "true");
	}
	/**
	 * Returns url path to project directory.
	 *
	 * Example: If website url is "www.example.com/stg/index.php", baseUrl will
	 * be "/stg/".
	 *
	 * @return string Url path to project directory
	 */
	public function getBaseUrl() {
		$baseUrl = $this->_config->baseUrl->getValue ();
		if (! empty ( $baseUrl )) {
			return $this->_config->baseUrl->getValue ();
		} else {
			return Zend_Controller_Front::getInstance ()->getBaseUrl ();
		}
	}
	/**
	 * Returns path to "public" directory.
	 *
	 * @return string Public directory path
	 */
	public function getPublicDir() {
		return $this->_config->publicDir->getValue ();
	}
	/**
	 * Returns host name.
	 *
	 * Example: If website url is "www.example.com", host name will
	 * be "example.com"
	 *
	 * @return string Host name
	 */
	public function getHostName() {
		return $this->_config->hostName->getValue ();
	}
	public function getTranslationsDir() {
		return $this->_config->translationsDir->getValue ();
	}
	/**
	 * Returns available modules information classes.
	 *
	 * @return array [module name] => SmartL_Application_Config_ModuleInfo
	 */
	public function getModules() {
		$this->_buildModules ();
		return $this->_modulesInfo;
	}
	/**
	 * Returns module data by name.
	 *
	 * @param string $name
	 * @return SmartL_Application_Config_ModuleInfo
	 */
	public function getModule($name) {
		$this->_buildModules ();
		if (! isset ( $this->_modulesInfo [$name] )) {
			require_once 'SmartL/Application/Config/Exception.php';
			throw new SmartL_Application_Config_Exception ( "Module with name {$name} was not found in config." );
		}
		return $this->_modulesInfo [$name];
	}
	/**
	 * Returns available connections configurations.
	 *
	 * @return array [connection string name] => array connection config
	 */
	public function getConnectionsConfig() {
		$this->_buildConnections ();
		return $this->_connections;
	}
	/**
	 * Returns default module information class.
	 *
	 * @return SmartL_Application_Config_ModuleInfo Default module info
	 */
	public function getDefaultModule() {
		$this->_buildModules ();
		return $this->_defaultModule;
	}
	/**
	 * Returns array of supported languages information classes.
	 * @return array [language-CULTURE] => SmartL_Application_Config_LanguageInfo
	 */
	public function getSupportedLanguages() {
		$this->_buildLanguages ();
		return $this->_languagesInfo;
	}
	/**
	 * Returns default language information class.
	 *
	 * @return SmartL_Application_Config_LanguageInfo Default language info
	 */
	public function getDefaultLanguage() {
		$this->_buildLanguages ();
		return $this->_defaultLanguage;
	}
	/**
	 * Returns given language with default culture.
	 *
	 * @param string $langId
	 * @return SmartL_Application_Config_LanguageInfo
	 */
	public function getDefaultCulture($langId) {
		$this->_buildLanguages ();
		if (! isset ( $this->_defaultCultures [$langId] )) {
			require_once 'SmartL/Application/Config/Exception.php';
			throw new SmartL_Application_Config_Exception ( "Language with given id {$langId} is not supported." );
		}
		return $this->_defaultCultures [$langId];
	}
	/**
	 * Returns true if given language (with culture) id matches some supported
	 * language id.
	 *
	 * @param string|SmartL_Application_Config_LanguageInfo $id Id of language or instance of it.
	 * @param boolean $compareCultures True if you want to compare also cultures.
	 * @return boolean True if given language id matches some supported language id
	 */
	public function isSupportedLanguage($id, $compareCultures = true) {
		$this->_buildLanguages ();
		if ($id instanceof SmartL_Application_Config_LanguageInfo) {
			if ($compareCultures) {
				$id = $id->getId ();
			} else {
				$id = $id->getLanguageId ();
			}
		}
		foreach ( $this->_languagesInfo as $data ) {
			if (($compareCultures and $data->equals ( $id )) or (! $compareCultures and $data->getLanguageId () == $id)) {
				return true;
			}
		}
		return false;
	}
	/**
	 * Returns available templates information classes.
	 *
	 * @return array [template name] => SmartL_Application_Config_TemplateInfo
	 */
	public function getTemplates() {
		$this->_buildTemplates ();
		return $this->_templates;
	}
	/**
	 * Returns actual template information class.
	 *
	 * @return SmartL_Application_Config_TemplateInfo Actual template info
	 */
	public function getActualTemplate() {
		$this->_buildTemplates ();
		return $this->_defaultTemplate;
	}
	/**
	 * Sets actual template by it's name.
	 *
	 * @param strign $name New actual template name
	 */
	public function setActualTemplate($name) {
		$this->_buildTemplates ();
		if (! key_exists ( $name, $this->_templates )) {
			require_once 'SmartL/Application/Exception.php';
			throw new SmartL_Application_Exception ( "Template with name {$name} doesn't exist in this config." );
		}
		$this->_defaultTemplate = $this->_templates [$name];
	}
	/**
	 * Returns access permissions defined in the configuration. If no were found,
	 * it returns FALSE.
	 *
	 * @return array [numeric] => string Array of role names
	 */
	public function getConfigAccessPermissions() {
		if (isset ( $this->_config->authorization ) and isset ( $this->_config->authorization->allow ))
			return $this->_config->authorization->getChildValues ( "allow" );
		else
			return array ();
	}
	/**
	 * Returns access restrictions defined in the configuration. If no were found,
	 * it returns FALSE.
	 *
	 * @return array [numeric] => string Array of role names
	 */
	public function getConfigAccessRestrictions() {
		if (isset ( $this->_config->authorization ) and isset ( $this->_config->authorization->deny ))
			return $this->_config->authorization->getChildValues ( "deny" );
		else
			return array ();
	}
	/**
	 * Returns information about behavior in case that access to some requested
	 * action was denied.
	 *
	 * It returns NULL when this information class wasn't set.
	 * @return SmartL_Application_Config_RequestInfo
	 */
	public function getAccessDeniedRequestInfo() {
		if (isset ( $this->_config->accessDenied )) {
			if (! isset ( $this->_config->accessDenied->controller ) or ! isset ( $this->_config->accessDenied->action ) or ! isset ( $this->_config->accessDenied->module )) {
				require_once 'SmartL/Application/Config/Exception.php';
				throw new SmartL_Application_Config_Exception ( "Attributes controller, action and module must be set inside accessDenied tag" );
			}
			$data = $this->_config->accessDenied;
			$result = new SmartL_Application_Config_RequestInfo ( );
			$result->setModule ( $data->module->getValue () );
			$result->setController ( $data->controller->getValue () );
			$result->setAction ( $data->action->getValue () );
			$result->setModule ( $data->module->getValue () );
			return $result;
		} else {
			return null;
		}
	}
	
	/**
	 * Returns configurations of all adapters in array.
	 * Key is name of adapter and value is it's config.
	 * 
	 * @return array
	 */
	public function getCacheAdaptersConfig() {
		$this->_buildCacheAdapters ();
		return $this->_cacheAdapters;
	}
	
	/**
	 * Global option for all cache adapters.
	 *
	 * @return boolean
	 */
	public function isCacheDisabled() {
		if (isset ( $this->_config->cache ) and isset ( $this->_config->cache->disabled )) {
			return strtolower ( $this->_config->cache->disabled->getValue () ) == 'true';
		} else {
			return false;
		}
	}
	
	/**
	 * Returns name of cache adapter that should be used to cache action outputs.
	 * If this option is not set in config, returns null.
	 *
	 * @return string|null
	 */
	public function getActionCacheName() {
		if (isset ( $this->_config->cache ) and isset ( $this->_config->cache->actionCache )) {
			return $this->_config->cache->actionCache->getValue ();
		} else {
			return null;
		}
	}
	
	/**
	 * Returns name of cache adapter that should be used to cache translate adapters.
	 * If option is not set in config, returns null.
	 *
	 * @return string
	 */
	public function getZendTranslateCacheName() {
		if (isset ( $this->_config->cache ) and isset ( $this->_config->cache->zendTranslateCache )) {
			return $this->_config->cache->zendTranslateCache->getValue ();
		} else {
			return null;
		}
	}
	
	/**
	 * Indicates whether urls should be always lower case.
	 * The default value is true!
	 * This can avoid duplicity the same urls with different letter case.
	 * If true, url plugins will always return lowercase urls and router will not accept url with other letter case.
	 * Use alwaysLowerUrl configuration option in configuration.
	 *
	 * @return boolean
	 */
	public function getAlwaysLowerUrlFlag()
	{
		if ( isset ( $this->_config->alwaysLowerUrl ) ) {
			return $this->_getBoolean($this->_config->alwaysLowerUrl->getValue());
		}
		return true;
	}
	
	//-----------------------------------------------------------//
	// private functions //
	

	/**
	 * Resets all data builded from loaded configuration.
	 */
	private function _reset() {
		$this->_languagesInfo = null;
		$this->_defaultLanguage = null;
		$this->_modulesInfo = null;
		$this->_connections = null;
		$this->_defaultConnectionStringName = null;
		$this->_templates = null;
		$this->_cacheAdapters = null;
	}
	
	/**
	 * Builds modules config data.
	 *
	 */
	private function _buildModules() {
		if ($this->_modulesInfo == null) {
			foreach ( $this->_config->modules as $name => $data ) {
				$module = new SmartL_Application_Config_ModuleInfo ( );
				$module->setName ( $name );
				$module->setControllersDir ( $this->_normalizePath ( $data->controllersDir->getValue () ) );
				$module->setViewsDir ( $this->_normalizePath ( $data->viewsDir->getValue () ) );
				$this->_modulesInfo [$name] = $module;
				if (isset ( $data->default ) and $data->default->getValue () == "true") {
					if ($this->_defaultModule != null) {
						require_once 'SmartL/Application/Config/Exception.php';
						throw new SmartL_Application_Config_Exception ( "There can't be 2 default modules in configuration." );
					}
					$this->_defaultModule = $module;
				}
			}
		}
	}
	
	/**
	 * Builds languages array and also sets default language.
	 *
	 * @uses SmartL_Application_Config_LanguageInfo
	 */
	private function _buildLanguages() {
		if ($this->_languagesInfo == null) {
			foreach ( $this->_config->languages as $langId => $langData ) {
				$langName = $langData->name->getValue ();
				//cultures
				$found = false; //if default culture was found
				foreach ( $langData->cultures as $cultId => $cultData ) {
					
					$language = new SmartL_Application_Config_LanguageInfo ( );
					$language->setLanguageId ( $langId );
					$language->setLanguageName ( $langName );
					$language->setCultureId ( $cultId );
					$language->setCultureName ( $cultData->name->getValue () );
					$this->_languagesInfo [$language->getId ()] = $language;
					
					if (isset ( $cultData->default ) and ($cultData->default->getValue () == "true")) {
						if ($found) {
							require_once 'SmartL/Application/Config/Exception.php';
							throw new SmartL_Application_Config_Exception ( "In language '{$langName}' can't be 2 default cultures." );
						}
						$found = true;
						$this->_defaultCultures [$language->getLanguageId ()] = $language;
					}
				}
				if (! $found) {
					require_once 'SmartL/Application/Config/Exception.php';
					throw new SmartL_Application_Config_Exception ( "In language '{$langName}' must be default culture." );
				}
				//Is it default language
				if (isset ( $langData->default ) && $langData->default->getValue () == "true") {
					if ($this->_defaultLanguage != null) {
						require_once 'SmartL/Application/Config/Exception.php';
						throw new SmartL_Application_Config_Exception ( "There can't be more than one default language in config." );
					}
					$this->_defaultLanguage = $this->_defaultCultures [$langId];
				}
			}
		}
	}
	
	/**
	 * Builds config connections strings data.
	 */
	private function _buildConnections() {
		if ($this->_connections !== null)
			return;
		$this->_connections = array ();
		if (! isset ( $this->_config->connections ) or ! is_array ( $this->_config->connections ))
			return;
		
		$defaults = array (); //store defaults for each adapter
		foreach ( $this->_config->connections as $name => $data ) {
			$config = $data->toArray ();
			if (isset ( $config ['options'] )) {
				$this->_connections [$name] ['config'] = $config ['options'];
			} else {
				$this->_connections [$name] ['config'] = array ();
			}
			//basic validation
			if (! isset ( $config ['adapter'] )) {
				require_once 'SmartL/Application/Config/Exception.php';
				throw new SmartL_Application_Config_Exception ( "In config each connection must have an adapter." );
			}
			$this->_connections [$name] ['adapter'] = $config ['adapter'];
			if (isset ( $data->default ) and strtolower ( $data->default->getValue () ) == "true") {
				//it is default
				//is it first default for this adapter?
				if (isset ( $defaults [$data->adapter->getValue ()] )) {
					require_once 'SmartL/Application/Config/Exception.php';
					throw new SmartL_Application_Config_Exception ( "In config can't be 2 default connections for one adapter." );
				}
				//save information about this default
				$defaults [$data->adapter->getValue ()] = true;
			}
		}
	
	}
	
	/**
	 * Normalize paths on first level of config: "publicDir","translationsDir","baseUrl","hostName".
	 * @uses _normalizePath
	 *
	 */
	private function _normalizePaths() {
		$paths = array ("publicDir", "translationsDir", "baseUrl", "hostName" );
		foreach ( $paths as $path ) {
			if ( !isset($this->_config->$path) or is_array($this->_config->$path) ) {
				require_once 'SmartL/Application/Config/Exception.php';
				throw new SmartL_Application_Config_Exception("Value of ".$path." is missing in configuration or has wrong rewriteMode.");
			}
			$this->_config->$path->setValue ( $this->_normalizePath ( $this->_config->$path->getValue () ) );
		}
	}
	
	/**
	 * Normalizes given path.
	 * Use this function for each path given in config.
	 *
	 * In this time normalization meas trimming slashes in the begin and in the end of path, but
	 * in future there can be inserted other logic.
	 *
	 * @param string $path Path to normalize.
	 * @return string Result of normalization.
	 */
	private function _normalizePath($path) {
		return trim ( $path, '/\\' );
	}
	
	/**
	 * Builds config templates data.
	 *
	 * @uses SmartL_Application_Config_TemplateInfo
	 */
	private function _buildTemplates() {
		if ($this->_templates !== null) {
			return;
		}
		$this->_templates = array ();
		if (! isset ( $this->_config->templates ))
			return;
		
		foreach ( $this->_config->templates as $name => $data ) {
			$tpl = new SmartL_Application_Config_TemplateInfo ( );
			$tpl->setName ( $name );
			$defaultSkin = null;
			$skins = array ();
			foreach ( $data->skins as $skinName => $skinData ) {
				$skins [] = $skinName;
				if (isset ( $skinData->default ) and $skinData->default->getValue () == "true") {
					if ($defaultSkin != null) {
						require_once 'SmartL/Application/Config/Exception.php';
						throw new SmartL_Application_Config_Exception ( "In template '{$tpl->getName()}' can't be 2 default skins." );
					}
					$defaultSkin = $skinName;
				}
			}
			$tpl->setSkins ( $skins );
			$tpl->setHasLayout ( (! isset ( $data->layout ) or ($data->layout->getValue () == "true")) );
			$tpl->setActualSkin ( $defaultSkin );
			
			$this->_templates [$name] = $tpl;
			if (isset ( $data->default ) and $data->default->getValue () == "true") {
				if ($this->_defaultTemplate != null) {
					require_once 'SmartL/Application/Config/Exception.php';
					throw new SmartL_Application_Config_Exception ( "There can't be 2 default templates in the configuration." );
				}
				$this->_defaultTemplate = $tpl;
			}
		}
	}
	
	private function _buildCacheAdapters() {
		if (isset ( $this->_cacheAdapters ))
			return;
		if (! isset ( $this->_config->cache ) or ! isset ( $this->_config->cache->adapters )) {
			$this->_cacheAdapters = array ();
			return;
		}
		
		foreach ( $this->_config->cache->adapters as $key => $adapter ) {
			if (! isset ( $adapter->backend )) {
				require_once 'SmartL/Application/Config/Exception.php';
				throw new SmartL_Application_Config_Exception ( "Cache adapter must define at least the backend name." );
			}
			$frontend = isset ( $adapter->frontend ) ? $adapter->frontend->getValue () : 'Core';
			$backendOptions = isset ( $adapter->backendOptions ) ? $adapter->backendOptions->toArray () : array ();
			$frontendOptions = isset ( $adapter->frontendOptions ) ? $adapter->frontendOptions->toArray () : array ();
			$disabled = isset ( $adapter->disabled ) ? strtolower ( $adapter->disabled->getValue () ) == 'true' : false;
			$this->_cacheAdapters [$key] = new SmartL_Application_Config_CacheAdapterInfo ( $key, $adapter->backend->getValue (), $frontend, $backendOptions, $frontendOptions, $disabled );
		}
	}
	
	/**
	 * Translates boolean given in string (loaded from configuration) to real boolean.
	 * Accepts True, true or 1 as true, other values leads into returning false.
	 *
	 * @return boolean
	 */
	private function _getBoolean($str)
	{
		return (strtolower($str) == 'true' or $str === '1');
	}
}